package de.htw.ds;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Comparator;


/**
 * <p>This static class provides conversions between object, binary and string
 * representations of IP addresses and IP socket addresses. IPv6 addresses
 * are binary represented in 128 bits, while IPv6 socket addresses require
 * 144 bits. Note that IPv4 addresses and socket addresses are represented
 * in their respective IPv6 compatibility representation. */
@TypeMetadata(copyright = "2009-2010 Sascha Baumeister, all rights reserved", version = "0.1.0", authors = "Sascha Baumeister")
public final class InetAddresses {
	private static final long serialVersionUID = 1L;
	private static final byte[] IPv4_LOOPBACK_ADDRESS = { 127, 0, 0, 1 };
	private static final byte[] COMPATIBILITY_MASK = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0 };

	/**
	 * The binary address length of an IPv4 address
	 */
	public static final int IPv4_ADDRESS_LENGTH = 4;

	/**
	 * The binary address length of an IPv6 address
	 */
	public static final int IPv6_ADDRESS_LENGTH = IPv4_ADDRESS_LENGTH * IPv4_ADDRESS_LENGTH;

	/**
	 * The binary address length of a socket address
	 */
	public static final int SOCKET_ADDRESS_LENGTH = IPv6_ADDRESS_LENGTH + 2;

	/**
	 * Comparator for use in sorted collections featuring IP address elements.
	 */
	public static class InetAddressComparator implements Comparator<InetAddress> {
		public int compare(final InetAddress address1, final InetAddress address2) {
			if (address1 == null) return address2 == null ? 0 : -1;
			if (address2 == null) return 1;
			return InetAddresses.compare(address1.getAddress(), address2.getAddress());
		}
	}

	/**
	 * Comparator for use in sorted collections featuring IP socket address elements.
	 */
	public static class InetSocketAddressComparator implements Comparator<InetSocketAddress> {
		public int compare(final InetSocketAddress socketAddress1, final InetSocketAddress socketAddress2) {
			if (socketAddress1 == null) return socketAddress2 == null ? 0 : -1;
			if (socketAddress2 == null) return 1;
			if (socketAddress1.isUnresolved() || socketAddress2.isUnresolved()) {
				return socketAddress1.getHostName().compareTo(socketAddress2.getHostName());
			} else {
				final byte[] binarySocketAddress1 = InetAddresses.toBinarySocketAddress(socketAddress1);
				final byte[] binarySocketAddress2 = InetAddresses.toBinarySocketAddress(socketAddress2);
				return InetAddresses.compare(binarySocketAddress1, binarySocketAddress2);
			}
		}
	}


	/**
	 * Private constructor prevents instantiation.
	 */
	private InetAddresses() {
		super();
	}


	/**
	 * Returns the local IP address. In case the local host cannot be resolved,
	 * the loopback address is returned. In case a security manager prevents this,
	 * the loopback address is constructed from a binary address. Note that this
	 * method will not throw any exceptions, especially in opposition to 
	 * {@link java.net.InetAddress#getLocalHost()}!
	 * @return the local IP address
	 */
	public static InetAddress getLocalAddress() {
		try {
			return InetAddress.getLocalHost();
		} catch (final UnknownHostException exception) {
			try {
				return InetAddress.getByAddress(IPv4_LOOPBACK_ADDRESS);
			} catch (final UnknownHostException nestedException) {
				throw new AssertionError();
			}
		}
	}


	/**
	 * Returns the given text representation of the given socket address, similar to
	 * {@link java.net.InetSocketAddress#toString()}. However, in opposition to the
	 * former either the canonical host name or the address is included in the result.
	 * The result can be parsed using {@link #toSocketAddress(String)}
	 * @param socketAddress the socket address
	 * @return the text representation
	 */
	public static String toString(final InetSocketAddress socketAddress) {
		return socketAddress.getAddress().getCanonicalHostName() + ":" + socketAddress.getPort();
	}


	/**
	 * Returns an IP socket address from the given text representation. The text
	 * representation is expected to be in the format <code>[address:]port</code>.
	 * In case the address part is omitted, the resulting IP socket address will
	 * be created using the local address.
	 * @param socketAddressText the text representation of an IP socket address
	 * @return the IP socket address
	 * @throws IllegalArgumentException if the port portion of the given socket
	 *    address text representation isn't a valid port within range [0, 65535].
	 * @throws UnknownHostException if no IP address for the host portion of the 
	 *    text representation could be found, or if a scope_id was specified for
	 *    a global IPv6 address.
	 * @see #toString(InetSocketAddress)
	 */
	public static InetSocketAddress toSocketAddress(final String socketAddressText) throws UnknownHostException {
		final InetAddress address;
		final int port;

		final int colonOffset = socketAddressText.lastIndexOf(':');
		try {
			if (colonOffset == -1) {
				port = Integer.parseInt(socketAddressText);
				address = InetAddresses.getLocalAddress();
			} else {
				port = Integer.parseInt(socketAddressText.substring(colonOffset + 1));
				address = InetAddress.getByName(socketAddressText.substring(0, colonOffset));
			}
			return new InetSocketAddress(address, port);
		} catch (final NumberFormatException exception) {
			throw new IllegalArgumentException();
		}
	}


	/**
	 * Returns the 144 bit (18 byte) binary IP socket address for the given IP
	 * socket address. If the given address is an IPv4 address, it's binary
	 * IPv6 compatibility address will be used to create the result. The result
	 * consists of 16 IP address bytes, and 2 subsequent IP port bytes (MSB at
	 * index 16, LSB at index 17). 
	 * @return the 144 bit binary IP socket address
	 */
	public static byte[] toBinarySocketAddress(final InetSocketAddress socketAddress) {
		final byte[] binaryAddress = socketAddress.getAddress().getAddress();
		final byte[] binarySocketAddress = Arrays.copyOf(COMPATIBILITY_MASK, SOCKET_ADDRESS_LENGTH);
		System.arraycopy(binaryAddress, 0, binarySocketAddress, IPv6_ADDRESS_LENGTH - binaryAddress.length, binaryAddress.length);
		binarySocketAddress[SOCKET_ADDRESS_LENGTH - 2] = (byte) (socketAddress.getPort() >>> 8);
		binarySocketAddress[SOCKET_ADDRESS_LENGTH - 1] = (byte) (socketAddress.getPort() >>> 0);
		return binarySocketAddress;
	}


	/**
	 * Returns an IP socket address from the given binary IP socket address. The
	 * latter is expected to consist of 144 bits (18 bytes) with the first 16
	 * representing an IPv6 address, followed by the port MSB, followed by the
	 * port LSB.
	 * @param binarySocketAddress a 144 bit binary IP socket address
	 * @return the IP socket address
	 * @throws NullPointerException if the given binary socket address is null
	 * @see #toBinarySocketAddress(InetSocketAddress)
	 */
	public static InetSocketAddress toSocketAddress(final byte[] binarySocketAddress) {
		try {
			final InetAddress address = InetAddress.getByAddress(Arrays.copyOf(binarySocketAddress, IPv6_ADDRESS_LENGTH));
			final int port = ((binarySocketAddress[binarySocketAddress.length - 2] & 0xFF) << 8) | ((binarySocketAddress[binarySocketAddress.length - 1] & 0xFF) << 0);
			return new InetSocketAddress(address, port);
		} catch (final UnknownHostException exception) {
			throw new AssertionError();
		}
	}


	/**
	 * Returns a service URI created from the given scheme, IP socket address, and
	 * absolute path.
	 * @param socketAddress the IP socket address
	 * @param scheme the URI scheme or null
	 * @param path the absolute path
	 * @return the service URI
	 * @throws NullPointerException if the given scheme or path is null
	 * @throws IllegalArgumentException if the given service path is not absolute
	 *     (does not start with a '/' character), or if the scheme or service path
	 *     violate RFC 2396 in any other way
	 */
	public static URI toServiceURI(final InetSocketAddress socketAddress, final String scheme, final String path) {
		if (scheme == null || path == null) throw new NullPointerException();
		try {
			return new URI(scheme, null, socketAddress.getAddress().getHostAddress(), socketAddress.getPort(), path, null, null);
		} catch (final URISyntaxException exception) {
			throw new IllegalArgumentException();
		}
	}


	/**
	 * Returns either the binary IPv6 address of a given IPv6 address, or the binary
	 * IPv6 compatibility address of a given IPv4 address. This ensures that in both
	 * cases a byte array of length 16 (128 bit) is returned, in opposition to
	 * {@link java.net.InetAddress#getAddress()}.
	 * @param address the IP address
	 * @return the 128 bit binary IP address
	 * @see java.net.InetAddress#getAddress()
	 */
	public static byte[] toBinaryAddress(final InetAddress address) {
		final byte[] binaryAddress = address.getAddress();
		if (binaryAddress.length == IPv6_ADDRESS_LENGTH) return binaryAddress;

		final byte[] binaryIPv6Address = COMPATIBILITY_MASK.clone();
		System.arraycopy(binaryAddress, 0, binaryIPv6Address, binaryIPv6Address.length - binaryAddress.length, binaryAddress.length);
		return binaryIPv6Address;
	}


	/**
	 * Compares the two byte array's content interpreted as right-aligned unsigned
	 * integer values. Returns +1 if bytes1 represents the larger integer value, or
	 * -1 if bytes2 represents the larger one. Returns zero if both byte arrays
	 * represent the same unsigned integer value, with leading bytes being treated as
	 * virtual zeros in case of differing byte array lengths.
	 * @param bytes1 a byte array
	 * @param bytes2 another byte array
	 * @throws NullPointerException if one of the given byte arrays is null
	 */
	public static int compare(final byte[] bytes1, final byte[] bytes2) {
		for (int index = bytes1.length - bytes2.length - 1; index >= 0; --index) {
			if (bytes1[index] != 0) return 1;
		}
		for (int index = bytes2.length - bytes1.length - 1; index >= 0; --index) {
			if (bytes2[index] != 0) return -1;
		}
		for (int index1 = bytes1.length - 1, index2 = bytes2.length - 1; index1 >= 0 && index2 >= 0; --index1, --index2) {
			final int byte1 = bytes1[bytes1.length - index1 - 1];
			final int byte2 = bytes2[bytes2.length - index2 - 1];
			if (byte1 > byte2) return  1;
			if (byte1 < byte2) return -1;
		}
		return 0;
	}
}